#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
Transforms Solr's CHANGELOG.md into Changes.html

Input is from CHANGELOG.md, output is to STDOUT
"""

import sys
import re
from pathlib import Path


class ChangelogParser:
    """Parse CHANGELOG.md generated by logchange"""

    RELEASE_PATTERN = re.compile(r'^\[(\d+(?:\.\d+)*(?:-[a-zA-Z0-9.]+)?)\](\s+-\s+(.+))?$')
    SECTION_PATTERN = re.compile(r'^###\s+(\w+(?:\s+\w+)*)\s*(?:\(\d+\s+changes?\))?')
    ITEM_PATTERN = re.compile(r'^###|^\[|^- ')

    def __init__(self):
        self.title = "Solr Changelog"
        self.releases = []
        self.preamble = None

    def _save_section(self, current_release, current_section, current_items):
        """Save current section to release if valid"""
        if current_release and current_section and current_items:
            current_release['sections'].append({
                'name': current_section,
                'items': current_items
            })

    def parse(self, content):
        """Parse CHANGELOG.md content"""
        lines = content.split('\n')
        current_release = None
        current_section = None
        current_items = []
        i = 0

        while i < len(lines):
            line = lines[i]
            stripped = line.strip()

            # Skip HTML comments
            if stripped.startswith('<!--') or stripped.startswith('-->'):
                i += 1
                continue

            # Extract preamble (text before first release)
            if not current_release and not self.preamble and stripped and not stripped.startswith('['):
                self.preamble = stripped
                i += 1
                continue

            # Match release header: [9.9.0] - 2025-07-24
            match = self.RELEASE_PATTERN.match(line)
            if match:
                self._save_section(current_release, current_section, current_items)
                if current_release:
                    self.releases.append(current_release)

                current_release = {
                    'version': match.group(1),
                    'date': match.group(3).strip() if match.group(3) else None,
                    'sections': []
                }
                current_section = None
                current_items = []
                i += 1
                continue

            # Match section header: ### Added (9 changes)
            match = self.SECTION_PATTERN.match(line)
            if match and current_release:
                self._save_section(current_release, current_section, current_items)
                current_section = match.group(1)
                current_items = []
                i += 1
                continue

            # Match list item
            if line.startswith('- ') and current_release:
                item_text = line[2:]
                i += 1
                # Collect continuation lines
                while i < len(lines) and not self.ITEM_PATTERN.match(lines[i]):
                    if lines[i].strip():
                        item_text += ' ' + lines[i].strip()
                    i += 1
                current_items.append(item_text)
                continue

            i += 1

        # Save last section and release
        self._save_section(current_release, current_section, current_items)
        if current_release:
            self.releases.append(current_release)


class HTMLGenerator:
    """Generate HTML from parsed changelog"""

    JIRA_URL_PREFIX = 'https://issues.apache.org/jira/browse/'
    GITHUB_PR_PREFIX = 'https://github.com/apache/solr/pull/'
    GITHUB_ISSUE_PREFIX = 'https://github.com/apache/solr/issues/'

    def __init__(self, title="Solr Changelog"):
        self.title = title
        self.first_relid = None
        self.second_relid = None
        # Issue extraction patterns: (pattern, prefix, format_string)
        self.issue_patterns = [
            (r'\[([A-Z]+-\d+)\]\(https://issues\.apache\.org/jira/browse/\1\)',
             self.JIRA_URL_PREFIX, '{0}'),
            (r'\[PR#(\d+)\]\(https://github\.com/apache/solr/pull/\1\)',
             self.GITHUB_PR_PREFIX, 'PR#{0}'),
            (r'\[GITHUB#(\d+)\]\(https://github\.com/apache/solr/issues/\1\)',
             self.GITHUB_ISSUE_PREFIX, 'GITHUB#{0}')
        ]

    def _format_issue_link(self, url_prefix, issue_id, label):
        """Format a single issue reference as an HTML anchor tag"""
        return f'<a href="{url_prefix}{issue_id}">{label}</a>'

    def _extract_markdown_issue(self, text):
        """
        Extract markdown-formatted JIRA/GitHub issues like [SOLR-123](url) or [PR#123](url).
        Returns (issue_link_html, text_without_issue) or (None, text) if not found.
        """
        for pattern, url_prefix, label_fmt in self.issue_patterns:
            match = re.search(pattern, text)
            if match:
                issue_id = match.group(1)
                label = label_fmt.format(issue_id)
                issue_html = self._format_issue_link(url_prefix, issue_id, label)
                text_without = (text[:match.start()] + text[match.end():]).strip()
                return issue_html, text_without

        return None, text

    def _extract_plain_pr_references(self, text):
        """
        Extract plain GitHub PR references like #123 or #123 #456.
        Only matches PRs that appear before the author list (before opening paren or at end).
        Returns (issue_link_html, text_without_issue) or (None, text) if not found.
        """
        # Pattern: #\d+ optionally followed by more #\d+ before opening paren or end of string
        pattern = r'#(\d+)(?:\s+#(\d+))*\s*(?=\(|$)'
        match = re.search(pattern, text)

        if not match:
            return None, text

        # Extract all PR numbers from the matched text
        pr_numbers = re.findall(r'#(\d+)', match.group(0))
        if not pr_numbers:
            return None, text

        # Format each PR as an HTML link and join with commas
        pr_links = [self._format_issue_link(self.GITHUB_PR_PREFIX, pr_num, f'PR#{pr_num}')
                    for pr_num in pr_numbers]
        issue_html = ', '.join(pr_links)

        # Remove the PR references from the text
        text_without = (text[:match.start()] + text[match.end():]).strip()
        return issue_html, text_without

    def extract_issue_from_text(self, text):
        """
        Extract the first issue reference from text.
        Tries in order: markdown JIRA/GitHub issues, plain GitHub PR references.
        Returns (issue_link_html, text_without_issue) or (None, text) if not found.
        """
        # Try markdown-formatted issues first
        issue_html, text_without = self._extract_markdown_issue(text)
        if issue_html:
            return issue_html, text_without

        # Fall back to plain GitHub PR references
        return self._extract_plain_pr_references(text)

    def _format_single_author(self, author_text):
        """
        Format a single author entry to HTML.
        Supports:
        - Plain name: "Jan Høydahl" -> "Jan Høydahl"
        - Markdown link: "[Jan Høydahl](url)" -> "<a href=\"url\">Jan Høydahl</a>"
        - Name with GitHub: "Jan Høydahl @janhoy" -> "<a href=\"https://github.com/janhoy\">Jan Høydahl</a>"
        - Link with GitHub: "[Jan Høydahl](url) @janhoy" -> "<a href=\"url\">Jan Høydahl</a> <a href=\"https://github.com/janhoy\">@janhoy</a>"
        """
        author_text = author_text.strip()

        # Extract markdown link: [text](url)
        markdown_link_match = re.search(r'\[([^\]]+)\]\(([^)]+)\)', author_text)
        # Extract GitHub handle: @username
        github_match = re.search(r'@(\w+)', author_text)

        if markdown_link_match:
            # Has markdown link
            link_text = markdown_link_match.group(1)
            link_url = markdown_link_match.group(2)
            html = f'<a href="{link_url}">{self.escape_html(link_text)}</a>'

            if github_match:
                # Has both markdown link and GitHub handle
                github_handle = github_match.group(1)
                html += f' <a href="https://github.com/{github_handle}">@{github_handle}</a>'

            return html
        elif github_match:
            # Has GitHub handle but no markdown link - extract name and link it to GitHub
            github_handle = github_match.group(1)
            # Remove the @handle part to get just the name
            name = author_text.replace(f'@{github_handle}', '').strip()
            return f'<a href="https://github.com/{github_handle}">{self.escape_html(name)}</a>'
        else:
            # Plain name with no links
            return self.escape_html(author_text)

    def _extract_one_author_group(self, text, start_pos):
        """
        Extract one author group starting from start_pos (pointing to an opening paren).
        Returns (author_content, end_pos) or (None, start_pos) if no valid group.
        Handles markdown links [text](url) inside the group.
        """
        if start_pos >= len(text) or text[start_pos] != '(':
            return None, start_pos

        paren_depth = 0
        bracket_depth = 0
        content = []

        for i in range(start_pos, len(text)):
            char = text[i]

            # Track brackets to know if we're inside [text]
            if char == '[' and bracket_depth >= 0:
                bracket_depth += 1
            elif char == ']' and bracket_depth > 0:
                bracket_depth -= 1
            # Only track paren depth outside brackets
            elif bracket_depth == 0:
                if char == '(':
                    paren_depth += 1
                elif char == ')':
                    paren_depth -= 1
                    if paren_depth == 0:
                        # Found matching closing paren
                        return ''.join(content[1:]).strip(), i  # Skip opening paren

            content.append(char)

        return None, start_pos

    def extract_authors(self, text):
        """Extract authors from trailing parentheses, handling markdown links [text](url)"""
        authors_list = []

        # Find all author groups at the end of the text
        # Work backwards from the end to find opening parentheses
        i = len(text) - 1

        # Skip trailing whitespace
        while i >= 0 and text[i] in ' \t\n\r':
            i -= 1

        if i < 0 or text[i] != ')':
            return None, text

        # Find all complete author groups by working backwards
        author_positions = []  # List of (start, end) positions

        while i >= 0:
            if text[i] == ')':
                # Find the matching opening paren for this closing paren
                paren_depth = 1
                bracket_depth = 0
                j = i - 1

                while j >= 0 and paren_depth > 0:
                    char = text[j]

                    # Track brackets
                    if char == ']':
                        bracket_depth += 1
                    elif char == '[':
                        bracket_depth -= 1
                    # Track parens outside brackets
                    elif bracket_depth == 0:
                        if char == ')':
                            paren_depth += 1
                        elif char == '(':
                            paren_depth -= 1

                    j -= 1

                if paren_depth == 0:
                    # Found matching opening paren at j+1
                    start_pos = j + 1

                    # Check if this is part of a markdown link [text](url)
                    # Markdown links have ] immediately before the (
                    if start_pos > 0 and text[start_pos - 1] == ']':
                        # This is a markdown link URL, not an author group
                        # Continue searching backwards
                        i = j
                    else:
                        # This is an author group
                        author_positions.insert(0, (start_pos, i))

                        # Move past this group
                        i = j

                        # Skip whitespace before next potential group
                        while i >= 0 and text[i] in ' \t\n\r':
                            i -= 1

                        # Check if there's another author group right before
                        if i >= 0 and text[i] != ')':
                            # No more author groups
                            break
                else:
                    break
            else:
                break

        # Now process the found author groups
        if author_positions:
            # Extract text before first author group
            first_start = author_positions[0][0]
            text_without_authors = text[:first_start].strip()

            # Extract and format each author group
            for start_pos, end_pos in author_positions:
                author_content = text[start_pos + 1:end_pos]

                # Split by comma or "and" for multiple authors in one group
                for author in re.split(r',\s*|\s+and\s+', author_content):
                    author = author.strip()
                    if author:
                        formatted_author = self._format_single_author(author)
                        authors_list.append(formatted_author)

            if authors_list:
                return authors_list, text_without_authors

        return None, text

    def format_changelog_item(self, item_text):
        """
        Format a changelog item from markdown to HTML
        Format: [ISSUE](url) description (author1) (author2)
        Output: <a href>ISSUE</a>: description<br><span class="attrib">(authors)</span>
        """
        # Extract the issue
        issue_html, text_after_issue = self.extract_issue_from_text(item_text)

        # Always try to extract authors, whether or not we found an issue
        authors_list, description = self.extract_authors(text_after_issue if issue_html else item_text)

        if issue_html:
            # We have an issue link
            description = re.sub(r'^[:\s]+', '', description).strip()
            html = f'{issue_html}: {self.escape_html(description)}'
        else:
            # No issue link found
            if authors_list:
                # We have authors but no issue - just use the description part
                html = self.escape_html(description)
            else:
                # No issue and no authors - linkify the full text
                return self.linkify_remaining_text(item_text)

        # Add authors if we have them
        if authors_list:
            # Authors are already formatted as HTML, don't escape
            html += f'<br /><span class="attrib">({", ".join(authors_list)})</span>'

        return html

    def linkify_remaining_text(self, text):
        """Linkify URLs and remaining JIRA references"""
        text = self.escape_html(text)

        # Link remaining JIRA issues
        text = re.sub(
            r'([A-Z]+-\d+)',
            lambda m: f'<a href="{self.JIRA_URL_PREFIX}{m.group(1)}">{m.group(1)}</a>',
            text
        )

        # Linkify URLs
        text = re.sub(
            r'(?<!["\'>])(https?://[^\s\)]+)',
            lambda m: f'<a href="{m.group(1)}">{m.group(1)}</a>',
            text
        )

        return text

    def convert_markdown_links(self, text):
        """
        Convert markdown links [text](url) to HTML links <a href="url">text</a>
        Also linkifies plain HTTP/HTTPS URLs
        Also escapes HTML in plain text portions
        """
        placeholders = {}
        placeholder_counter = [0]

        def protect_with_placeholder(content):
            placeholder = f"__PLACEHOLDER_{placeholder_counter[0]}__"
            placeholders[placeholder] = content
            placeholder_counter[0] += 1
            return placeholder

        # Pattern: [text](url)
        def replace_markdown_link(match):
            link_text = match.group(1)
            link_url = match.group(2)
            html_link = f'<a href="{link_url}">{self.escape_html(link_text)}</a>'
            return protect_with_placeholder(html_link)

        # Replace all markdown links first
        result = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', replace_markdown_link, text)

        # Now handle plain URLs
        def replace_url(match):
            url = match.group(1)
            html_link = f'<a href="{url}">{url}</a>'
            return protect_with_placeholder(html_link)

        # Match HTTP/HTTPS URLs not already in links
        result = re.sub(r'(?<![">])(https?://[^\s\)]+)', replace_url, result)

        # Escape HTML in remaining text
        result = self.escape_html(result)

        # Restore the protected tags
        for placeholder, tag in placeholders.items():
            result = result.replace(placeholder, tag)

        return result

    def escape_html(self, text):
        """Escape HTML angle brackets to prevent rendering issues"""
        # Only escape < and > to avoid breaking markdown links and quotes
        text = text.replace('<', '&lt;')
        text = text.replace('>', '&gt;')
        return text

    def generate_header(self, preamble=None):
        """Generate HTML header"""
        first_relid_regex = re.escape(self.first_relid or 'trunk')
        first_relid_regex = first_relid_regex.replace('\\', '\\\\')
        second_relid_regex = re.escape(self.second_relid or '')
        second_relid_regex = second_relid_regex.replace('\\', '\\\\')

        newer_version_regex = f"^(?:{first_relid_regex}"
        if self.second_relid:
            newer_version_regex += f"|{second_relid_regex}"
        newer_version_regex += ")"

        html = f'''<!--
**********************************************************
** WARNING: This file is generated from CHANGELOG.md by the
**          Python script 'changes2html.py'.
**          Do *not* edit this file!
**********************************************************

****************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
****************************************************************************
-->
<!DOCTYPE html>
<html lang="en">
<head>
  <title>Apache Solr Release Notes</title>
  <link rel="stylesheet" href="ChangesFancyStyle.css" title="Fancy">
  <link rel="alternate stylesheet" href="ChangesSimpleStyle.css" title="Simple">
  <link rel="alternate stylesheet" href="ChangesFixedWidthStyle.css" title="Fixed Width">
  <META http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
  <SCRIPT>
    function toggleList(id) {{
      listStyle = document.getElementById(id + '.list').style;
      anchor = document.getElementById(id);
      if (listStyle.display == 'none') {{
        listStyle.display = 'block';
        anchor.title = 'Click to collapse';
        location.href = '#' + id;
      }} else {{
        listStyle.display = 'none';
        anchor.title = 'Click to expand';
      }}
      var expandButton = document.getElementById('expand.button');
      expandButton.disabled = false;
      var collapseButton = document.getElementById('collapse.button');
      collapseButton.disabled = false;
    }}

    function collapseAll() {{
      var unorderedLists = document.getElementsByTagName("ul");
      for (var i = 0; i < unorderedLists.length; i++) {{
        if (unorderedLists[i].className != 'bulleted-list')
          unorderedLists[i].style.display = "none";
        else
          unorderedLists[i].style.display = "block";
      }}
      var orderedLists = document.getElementsByTagName("ol");
      for (var i = 0; i < orderedLists.length; i++)
        orderedLists[i].style.display = "none";
      var olderList = document.getElementById("older.list");
      if (olderList) olderList.style.display = "none";
      var anchors = document.getElementsByTagName("a");
      for (var i = 0 ; i < anchors.length; i++) {{
        if (anchors[i].id != '')
          anchors[i].title = 'Click to expand';
      }}
      var collapseButton = document.getElementById('collapse.button');
      collapseButton.disabled = true;
      var expandButton = document.getElementById('expand.button');
      expandButton.disabled = false;
    }}

    function expandAll() {{
      var unorderedLists = document.getElementsByTagName("ul");
      for (var i = 0; i < unorderedLists.length; i++)
        unorderedLists[i].style.display = "block";
      var orderedLists = document.getElementsByTagName("ol");
      for (var i = 0; i < orderedLists.length; i++)
        orderedLists[i].style.display = "block";
      var olderList = document.getElementById("older.list");
      if (olderList) olderList.style.display = "block";
      var anchors = document.getElementsByTagName("a");
      for (var i = 0 ; i < anchors.length; i++) {{
        if (anchors[i].id != '')
          anchors[i].title = 'Click to collapse';
      }}
      var expandButton = document.getElementById('expand.button');
      expandButton.disabled = true;
      var collapseButton = document.getElementById('collapse.button');
      collapseButton.disabled = false;
    }}

    var newerRegex = new RegExp("{newer_version_regex}");
    function isOlder(listId) {{
      return ! newerRegex.test(listId);
    }}

    function escapeMeta(s) {{
      return s.replace(/([.*+?^${{}}()|[\\\\]\\\\\\/])/g, '\\\\\\\\$1');
    }}

    function shouldExpand(currentList, currentAnchor, listId) {{
      var listName = listId.substring(0, listId.length - 5);
      var parentRegex = new RegExp("^" + escapeMeta(listName) + "\\\\\\\\.");
      return currentList == listId
             || (isOlder(currentAnchor) && listId == 'older.list')
             || parentRegex.test(currentAnchor);
    }}

    function collapse() {{
      /* Collapse all but the first and second releases. */
      var unorderedLists = document.getElementsByTagName("ul");
      var currentAnchor = location.hash.substring(1);
      var currentList = currentAnchor + ".list";

      for (var i = 0; i < unorderedLists.length; i++) {{
        var list = unorderedLists[i];
        /* Collapse the current item, unless either the current item is one of
         * the first two releases, or the current URL has a fragment and the
         * fragment refers to the current item or one of its ancestors.
         */
        if (list.id != '{self.first_relid}.list'
            && list.id != '{self.second_relid}.list'
            && list.className != 'bulleted-list'
            && (currentAnchor == ''
                || ! shouldExpand(currentList, currentAnchor, list.id))) {{
          list.style.display = "none";
        }}
      }}
      var orderedLists = document.getElementsByTagName("ol");
      for (var i = 0; i < orderedLists.length; i++) {{
        var list = orderedLists[i];
        /* Collapse the current item, unless the current URL has a fragment
         * and the fragment refers to the current item or one of its ancestors.
         */
        if (currentAnchor == ''
            || ! shouldExpand(currentList, currentAnchor, list.id)) {{
          list.style.display = "none";
        }}
      }}
      var olderList = document.getElementById("older.list");
      if (olderList) olderList.style.display = "none";
      /* Add "Click to collapse/expand" tooltips to the release/section headings */
      var anchors = document.getElementsByTagName("a");
      for (var i = 0 ; i < anchors.length; i++) {{
        var anchor = anchors[i];
        if (anchor.id != '') {{
          if (anchor.id == '{self.first_relid}' || anchor.id == '{self.second_relid}') {{
            anchor.title = 'Click to collapse';
          }} else {{
            anchor.title = 'Click to expand';
          }}
        }}
      }}

      /* Insert "Expand All" and "Collapse All" buttons */
      var buttonsParent = document.getElementById('buttons.parent');
      if (buttonsParent) {{
        var expandButton = document.createElement('button');
        expandButton.appendChild(document.createTextNode('Expand All'));
        expandButton.onclick = function() {{ expandAll(); }}
        expandButton.id = 'expand.button';
        buttonsParent.appendChild(expandButton);
        var collapseButton = document.createElement('button');
        collapseButton.appendChild(document.createTextNode('Collapse All'));
        collapseButton.onclick = function() {{ collapseAll(); }}
        collapseButton.id = 'collapse.button';
        buttonsParent.appendChild(collapseButton);
      }}
    }}

    window.onload = collapse;
  </SCRIPT>
</head>
<body>

<h1>Apache Solr Release Notes</h1>

<div id="buttons.parent"></div>

'''
        # Add preamble if present
        if preamble:
            # Convert markdown links to HTML links
            preamble_html = self.convert_markdown_links(preamble)
            html += f'<p>{preamble_html}</p>\n\n'

        return html

    def _format_section(self, relid, section_name, items):
        """Format a single section with items"""
        sectid = section_name.lower().replace(' ', '_')
        html = [f'  <li><a id="{relid}.{sectid}" href="javascript:toggleList(\'{relid}.{sectid}\')">'
                f'{self.escape_html(section_name)}</a>']
        html.append(f'&nbsp;&nbsp;&nbsp;({len(items)})\n')
        html.append(f'    <ul id="{relid}.{sectid}.list">\n')
        for item in items:
            html.append(f'      <li>{self.format_changelog_item(item)}</li>\n')
        html.append('    </ul>\n')
        return ''.join(html)

    def generate_releases(self, releases):
        """Generate HTML for releases"""
        html = []
        relcnt = 0

        for release in releases:
            version = release.get('version')
            if not version:
                continue

            relcnt += 1
            if relcnt == 3:
                html.append('<h2><a id="older" href="javascript:toggleList(\'older\');">Older Releases</a></h2>\n')
                html.append('<div id="older.list">\n')

            header = 'h3' if relcnt > 2 else 'h2'
            relid = f'v{version}'.replace(' ', '_').lower()
            date = release.get('date', '')

            # Build release header
            html.append(f'<{header}><a id="{relid}" href="javascript:toggleList(\'{relid}\')">'
                       f'Release {self.escape_html(version)}')
            if date:
                html.append(f' [{self.escape_html(date)}]')
            html.append(f'</a></{header}>\n')
            html.append(f'<ul id="{relid}.list">\n')

            # Render sections
            for section in release.get('sections', []):
                if section.get('name'):
                    html.append(self._format_section(relid, section['name'], section.get('items', [])))

            html.append('</ul>\n')

        if relcnt > 2:
            html.append('</div>\n')

        return ''.join(html)

    def generate(self, releases, title, preamble=None):
        """Generate complete HTML"""
        self.title = title or "Solr Changelog"

        # Determine first and second release IDs for collapsing
        if releases:
            self.first_relid = f'v{releases[0].get("version", "trunk")}'.replace(' ', '_').lower()
        if len(releases) > 1:
            self.second_relid = f'v{releases[1].get("version", "trunk")}'.replace(' ', '_').lower()
        else:
            self.second_relid = self.first_relid

        html_parts = [
            self.generate_header(preamble),
            self.generate_releases(releases),
            '</body>\n</html>\n'
        ]

        return ''.join(html_parts)


def main():
    """Main entry point"""
    if len(sys.argv) < 2:
        # Try to read from CHANGELOG.md in current directory
        changelog_file = Path('CHANGELOG.md')
        if not changelog_file.exists():
            print("Usage: changes2html.py <changelog-file>", file=sys.stderr)
            sys.exit(1)
    else:
        changelog_file = Path(sys.argv[1])

    if not changelog_file.exists():
        print(f"Error: {changelog_file} not found", file=sys.stderr)
        sys.exit(1)

    # Read changelog
    with open(changelog_file, 'r', encoding='utf-8') as f:
        content = f.read()

    # Parse
    parser = ChangelogParser()
    parser.parse(content)

    # Generate HTML
    generator = HTMLGenerator()
    html = generator.generate(parser.releases, parser.title, parser.preamble)

    # Output
    print(html)


if __name__ == '__main__':
    main()
